package jp.dodododo.xml.impl;

import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import jp.dodododo.dao.object.ObjectDesc;
import jp.dodododo.dao.object.ObjectDescFactory;
import jp.dodododo.dao.object.PropertyDesc;
import jp.dodododo.dao.util.ClassUtil;
import jp.dodododo.janerics.GenericsTypeUtil;
import jp.dodododo.xml.DtdPathSet;
import jp.dodododo.xml.annotation.Attribute;
import jp.dodododo.xml.annotation.Characters;
import jp.dodododo.xml.annotation.Element;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler;

public class Handler<T> extends DefaultHandler {

	private T result;

	private DtdPathSet dtdPathSet;

	private Locator locator;

	private Class<T> clazz;

	private List<Object> elementList = new LinkedList<Object>();

	public Handler(Class<T> clazz, DtdPathSet dtdPathSet) {
		this.dtdPathSet = dtdPathSet;
		this.clazz = clazz;
	}

	public T getResult() {
		return result;
	}

	@Override
	public void setDocumentLocator(Locator locator) {
		this.locator = locator;
	}

	@Override
	public InputSource resolveEntity(String publicId, String systemId) {
		if (publicId != null) {
			String dtdPath = dtdPathSet.getDtdPath(publicId);
			if (dtdPath != null) {
				return createInputSource(getResourceAsStream(dtdPath), systemId);
			}
		}
		if (systemId == null) {
			return null;
		}
		if (systemId.startsWith("file:")) {
			URL url = null;
			try {
				url = new URL(systemId);
			} catch (MalformedURLException e) {
				throw new RuntimeException(e);
			}
			String path = url.getFile();
			if (path.startsWith("/")) {
				path = path.substring(1);
			}
			return createInputSource(getResourceAsStream(path), systemId);
		}

		return new InputSource(getResourceAsStream(systemId));
	}

	private InputStream getResourceAsStream(String path) {
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		return loader.getResourceAsStream(path);
	}

	private InputSource createInputSource(InputStream stream, String systemId) {
		InputSource is = new InputSource(stream);
		is.setSystemId(systemId);
		return is;
	}

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) {
		@SuppressWarnings("unused")
		int lineNumber = locator.getLineNumber();
		characters = new StringBuilder();

		Object currentElement = getCurrentElement(qName);
		if (currentElement == null) {
			return;
		}
		ObjectDesc desc = ObjectDescFactory.getObjectDesc(currentElement);

		for (int i = 0; i < attributes.getLength(); i++) {
			String attributeName = attributes.getQName(i);
			if (hasPropertyDesc(desc, attributeName, Attribute.class) == false) {
				continue;
			}
			PropertyDesc propertyDesc = getPropertyDesc(desc, attributeName, Attribute.class);
			if (propertyDesc.isWritable() == false) {
				continue;
			}
			String value = attributes.getValue(attributeName);
			propertyDesc.setValue(currentElement, value);
		}
	}

	private <A extends Annotation> PropertyDesc getPropertyDesc(ObjectDesc desc, String propertyName, Class<A> annotation) {
		List<PropertyDesc> propertyDescs = desc.getPropertyDescs(annotation);
		for (PropertyDesc propertyDesc : propertyDescs) {
			A a = propertyDesc.getAnnotation(annotation);
			if (equalValue(a, propertyName) == true) {
				return propertyDesc;
			}
		}
		PropertyDesc propertyDesc = desc.getPropertyDesc(propertyName);
		return propertyDesc;
	}

	private <A extends Annotation> boolean hasPropertyDesc(ObjectDesc desc, String propertyName, Class<A> annotation) {
		List<PropertyDesc> propertyDescs = desc.getPropertyDescs(annotation);
		for (PropertyDesc propertyDesc : propertyDescs) {
			A a = propertyDesc.getAnnotation(annotation);
			if (equalValue(a, propertyName) == true) {
				return true;
			}
		}
		return desc.hasPropertyDesc(propertyName);
	}

	private boolean equalValue(Annotation a, String value) {
		if (a instanceof Attribute) {
			Attribute attribute = (Attribute) a;
			return attribute.value().equals(value);
		} else if (a instanceof Element) {
			Element element = (Element) a;
			return element.value().equals(value);
		} else {
			throw new IllegalArgumentException(a.toString());
		}
	}

	private Object getCurrentElement(String qName) {
		Object ret = null;
		if (elementList.size() == 0) {
			this.result = ClassUtil.newInstance(clazz);
			ret = this.result;
			this.elementList.add(ret);
		} else {
			Object recentElement = elementList.get(elementList.size() - 1);
			ObjectDesc desc = ObjectDescFactory.getObjectDesc(recentElement);
			PropertyDesc propertyDesc = null;
			if (hasPropertyDesc(desc, qName, Element.class)) {
				propertyDesc = getPropertyDesc(desc, qName, Element.class);
			} else if (hasPropertyDesc(desc, qName.replaceAll("-", ""), Element.class)) {
				propertyDesc = getPropertyDesc(desc, qName.replaceAll("-", ""), Element.class);
			} else {
				return null;
			}
			Class<?> propertyType = propertyDesc.getPropertyType();
			if (propertyDesc.isCollectionType() == true) {
				Method readMethod = propertyDesc.getReadMethod();
				Type genericReturnType = readMethod.getGenericReturnType();
				Type elementTypeOfCollection = GenericsTypeUtil.getElementTypeOfCollection(genericReturnType);
				ret = ClassUtil.newInstance(GenericsTypeUtil.getRawClass(elementTypeOfCollection));
				this.elementList.add(ret);
				List<Object> list = propertyDesc.getValue(recentElement);
				if (list == null) {
					list = new ArrayList<Object>();
					propertyDesc.setValue(recentElement, list);
				}
				list.add(ret);
			} else {
				ret = ClassUtil.newInstance(propertyType);
				this.elementList.add(ret);
				propertyDesc.setValue(recentElement, ret);
			}
		}

		return ret;
	}

	private StringBuilder characters = null;

	@Override
	public void characters(char[] ch, int start, int length) {
		characters.append(ch, start, length);
	}

	@Override
	public void endElement(String uri, String localName, String qName) {
		if (elementList.size() == 0) {
			return;
		}
		Object object = elementList.get(elementList.size() - 1);
		ObjectDesc desc = ObjectDescFactory.getObjectDesc(object);
		if (desc.hasPropertyDesc(Characters.class) == true) {
			PropertyDesc propertyDesc = desc.getPropertyDesc(Characters.class);
			propertyDesc.setValue(object, characters.toString().trim());
		} else if (desc.hasPropertyDesc("characters") == true) {
			PropertyDesc propertyDesc = desc.getPropertyDesc("characters");
			if (propertyDesc.isAnnotationPresent(Element.class)) {
			} else if (propertyDesc.isAnnotationPresent(Attribute.class)) {
			} else {
				propertyDesc.setValue(object, characters.toString().trim());
			}
		}
		elementList.remove(elementList.size() - 1);
	}
}
